// Copyright (C) 2010 David Sugar, Tycho Softworks.
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include "server.h"

#ifdef  HAVE_PWD_H
#include <pwd.h>
#include <grp.h>
#endif

#define CONTROL_PATH DEFAULT_VARPATH "/run/apennine"
#define CONFIG_FILE DEFAULT_CFGPATH "/apennine.conf"
#define PREFIX_PATH DEFAULT_VARPATH "/lib/apennine"

#ifdef  _MSWINDOWS_
#define PIDFILE_PATH    NULL
#define LIBEXT ".dll"
#else
#define PIDFILE_PATH    CONTROL_PATH "/pidfile"
#define LIBEXT ".so"
#endif

namespace apennine {

// so many options to choose from...

static shell::flagopt helpflag('h',"--help",    _TEXT("display this list"));
static shell::flagopt althelp('?', NULL, NULL);
static shell::flagopt v4flag('4', NULL, _TEXT("ipv4 binding only"));
static shell::flagopt v6flag('6', NULL, _TEXT("ipv6 binding only"));
static shell::flagopt backflag('b', "--background", _TEXT("run in background"));
static shell::stringopt controlpath(0, "--control", _TEXT("control file path"), "path", CONTROL_PATH);
static shell::stringopt configpath('C', "--config", _TEXT("config file path"), "path", CONFIG_FILE);
static shell::numericopt concurrency('c', "--concurrency", _TEXT("process concurrency"), "level");
static shell::stringopt prefixpath('D', "--prefix", _TEXT("working directory"),"path", PREFIX_PATH);
static shell::flagopt foreflag('f', "--foreground", _TEXT("run in foreground"));
#ifdef  HAVE_PWD_H
static shell::stringopt group('g', "--group", _TEXT("use specified group permissions"), "groupid", NULL);
#endif
static shell::stringopt ifaces('I', "--interfaces", _TEXT("network addresses to bind"), "address", "*");
static shell::stringopt libexecpath(0, "--libexec", _TEXT("execution path"), "path", DEFAULT_LIBEXEC);
static shell::stringopt loglevel('l', "--logging", _TEXT("set log level"), "level", "err");
static shell::stringopt modules('m', "--modules", _TEXT("specify modules to load"), "names", "*");
static shell::stringopt pluginpath(0, "--plugins", _TEXT("path to plugins"), "path", DEFAULT_PLUGINS);
static shell::numericopt port('P', "--port", _TEXT("network port to bind (80)"), "port", 80);
static shell::counteropt priority('p', "--priority", _TEXT("set priority level"), "level");
static shell::stringopt realm('R', "--realm", _TEXT("authentication realm if set"), "text", NULL);
static shell::flagopt restart('r', "--restartable", _TEXT("set to restartable process"));
static shell::numericopt ssl('S', "--ssl", _TEXT("use ssl stack"), "port", 0);
#ifdef HAVE_PWD_H
static shell::stringopt user('u', "--user", _TEXT("user to run as"), "userid", NULL);
#endif
static shell::flagopt verbose('v', NULL, _TEXT("set verbosity, can be used multiple times"), false);
static shell::numericopt debuglevel('x', "--debug", _TEXT("set debug level directly"), "level", 0);

static const char *pidfile = PIDFILE_PATH;

#if defined(HAVE_SETRLIMIT) && defined(DEBUG)
#include <sys/time.h>
#include <sys/resource.h>

static void corefiles(void)
{
    struct rlimit core;

    assert(getrlimit(RLIMIT_CORE, &core) == 0);
#ifdef  MAX_CORE_SOFT
    core.rlim_cur = MAX_CORE_SOFT;
#else
    core.rlim_cur = RLIM_INFINITY;
#endif
#ifdef  MAX_CORE_HARD
    core.rlim_max = MAX_CORE_HARD;
#else
    core.rlim_max = RLIM_INFINITY;
#endif
    assert(setrlimit(RLIMIT_CORE, &core) == 0);
}
#else
static void corefiles(void)
{
}
#endif

static void usage(void)
{
#if defined(HAVE_EXECVP) && defined(DEBUG)
    printf("%s\n", _TEXT("Usage: apennine-daemon [debug] [options]"));
#else
    printf("%s\n", _TEXT("Usage: apennine-daemon [options]"));
#endif
    printf("%s\n\n", _TEXT("Start httpd/xmlrpc service"));
#if defined(HAVE_EXECVP) && defined(DEBUG)
    printf("%s", _TEXT(
        "Debug:\n"
        "  --dbg            execute command in debugger\n"
        "  --memcheck       execute with valgrind memory check\n"
        "  --memleak        execute with valgrind leak detection\n"
        "\n"
    ));
#endif
    printf("%s\n", _TEXT("Options:"));
    shell::help();
    printf("\n%s\n", _TEXT("Report bugs to dyfet@gnu.org"));
    exit(0);
}

static void init(int argc, char **argv, bool detached, shell::mainproc_t svc = NULL)
{
    shell::bind("apennine");
    secure::init();
    corefiles();

    bool daemon = true;
    const char *cp;
    shell args;

#if defined(DEBUG)
    if(eq(argv[1], "-gdb") || eq(argv[1], "--gdb") || eq(argv[1], "-dbg") || eq(argv[1], "--dbg")) {
        char *dbg[] = {(char *)"gdb", (char *)"--args", NULL};
        const char *cp = args.getenv("DEBUGGER");
        if(cp && *cp)
            dbg[0] = (char *)cp;
        args.restart(argv[0], &argv[2], dbg);
    }

    if(eq(argv[1], "-memcheck") || eq(argv[1], "--memcheck")) {
        char *mem[] = {(char *)"valgrind", (char *)"--tool=memcheck", NULL};
        args.restart(argv[0], &argv[2], mem);
    }

    if(eq(argv[1], "-memleak") || eq(argv[1], "--memleak")) {
        char *mem[] = {(char *)"valgrind",
            (char *)"--tool=memcheck", (char *)"--leak-check=yes", NULL};
        args.restart(argv[0], &argv[2], mem);
    }
#endif

    // cheat out shell parser...
    argv[0] = (char *)"apennine";

#ifdef  HAVE_PWD_H
    struct passwd *pwd = getpwuid(getuid());
    umask(007);

    if(getuid() && pwd && pwd->pw_dir && *pwd->pw_dir == '/') {
        prefixpath.set(strdup(pwd->pw_dir));
        if(!eq(pwd->pw_shell, "/bin/false") && !eq(pwd->pw_dir, "/var/", 5) && !eq(pwd->pw_dir, "/srv/", 5)) {
            umask(077);

            configpath.set(strdup((String)pwd->pw_dir + "/.apenninerc"));
            prefixpath.set(strdup((String)pwd->pw_dir + "/.apennine"));
            controlpath.set(strdup((String)"/tmp/apennine-" + (const char *)pwd->pw_name));
            pidfile = strdup((String)*controlpath + "/pidfile");
            port.set(1080); // outside reserved range...
            daemon = false;
        };
    }

#else
#ifdef _MSWINDOWS_
    const char *app = args.getenv("APPDATA");
    if(app && *app) {
        cp = strdup((String)app + (String)"\\apennine");
        prefixpath.set(cp);
        controlpath.set(cp);
    }
    configpath.set("c:\\Program Files\\Apennine\\apennine.ini");
    libexecpath.set("c:\\Program Files\\Apennine\\Libexec");
    pluginpath.set("c:\\Program Files\\Apennine\\Plugins");
#endif
    if(argv[1])
        daemon = false;
#endif

#ifdef  HAVE_PWD_H
    cp = args.getenv("GROUP");
    if(cp && *cp)
        group.set(cp);

    cp = args.getenv("USER");
    if(cp && *cp)
        user.set(cp);
#endif
    cp = args.getenv("INTERFACES");
    if(cp && *cp)
        ifaces.set(cp);

    cp = args.getenv("PORT");
    if(cp && *cp)
        port.set(atoi(cp));

    cp = args.getenv("SSL");
    if(cp && *cp)
        ssl.set(atoi(cp));

    cp = args.getenv("REALM");
    if(cp && *cp)
        realm.set(cp);

    cp = args.getenv("CONFIG");
    if(cp && *cp)
        configpath.set(cp);

    cp = args.getenv("CONCURRENCY");
    if(cp && *cp)
        concurrency.set(atol(cp));

    cp = args.getenv("PRIORITY");
    if(cp && *cp)
        priority.set(atol(cp));

    cp = args.getenv("VERBOSE");
    if(cp && *cp)
        loglevel.set(strdup(cp));

    cp = args.getenv("LOGGING");
    if(cp && *cp)
        loglevel.set(strdup(cp));

    cp = args.getenv("LIBEXEC");
    if(cp && *cp)
        libexecpath.set(cp);

    cp = args.getenv("PLUGINS");
    if(cp && *cp)
        pluginpath.set(cp);

    cp = args.getenv("MODULES");
    if(cp && *cp)
        modules.set(cp);

    args.parse(argc, argv);

    if(is(helpflag) || is(althelp) || args.argc() > 0)
        usage();

    // now fetch config file for any overrides in [server] section...
    // these will be set only if the switch option was NOT modified...

    if(fsys::is_file(*configpath))
        Service::config.load(*configpath);

    keydata *keys = Service::config.get("server");

    if(keys) {
        cp = keys->get("port");
        if(cp && *cp && !is(port))
            port.set(atoi(cp));

        cp = keys->get("ssl");
        if(cp && *cp && !is(ssl))
            ssl.set(atoi(cp));

        cp = keys->get("realm");
        if(cp && *cp && !is(realm))
            realm.set(cp);

        cp = keys->get("interfaces");
        if(cp && *cp && !is(ifaces))
            ifaces.set(cp);

        cp = keys->get("priority");
        if(cp && *cp && !is(priority))
            priority.set(atoi(cp));

        cp = keys->get("concurrency");
        if(cp && *cp && !is(concurrency))
            concurrency.set(atoi(cp));

        cp = keys->get("logging");
        if(cp && *cp && !is(loglevel))
            loglevel.set(cp);

        cp = keys->get("modules");
        if(cp && *cp && !is(modules))
            modules.set(cp);

#ifdef  HAVE_PWD_H
        cp = keys->get("user");
        if(cp && *cp && !is(user))
            user.set(cp);

        cp = keys->get("group");
        if(cp && *cp && !is(group))
            group.set(cp);
#endif
    }

    // check validity of some options...

    if(*concurrency < 0)
        shell::errexit(1, "apennine: concurrency: %ld: %s\n",
            *concurrency, _TEXT("negative levels invalid"));

    if(*port == *ssl)
        port.set(0);

    if(*port < 0 || *port > 65000)
        shell::errexit(1, "apennine: port: %ld: %s\n",
            *port, _TEXT("invalid network port"));

    if(*ssl < 0 || *ssl > 65000)
        shell::errexit(1, "apennine: port: %ld: %s\n",
            *ssl, _TEXT("invalid ssl network port"));

    // make sure thread and scheduler properties set to be inherited

    if(*concurrency > 0)
        Thread::concurrency(*concurrency);

    shell::priority(*priority);

#ifdef  SCHED_RR
    if(*priority > 0)
        Thread::policy(SCHED_RR);
#endif

    // if neither set, set both...
    if(!is(v4flag) && !is(v6flag)) {
        v4flag.set();
        v6flag.set();
    }

    if(is(backflag))
        daemon = true;

    if(is(foreflag))
        daemon = false;

    // lets play with verbose level and logging options

    if(is(verbose))
        verbose.set(*verbose + (unsigned)shell::INFO);
    else {
        if(atoi(*loglevel) > 0)
            verbose.set(atoi(*loglevel));
        else if(eq(*loglevel, "0") || eq(*loglevel, "no", 2) || eq(*loglevel, "fail", 4))
            verbose.set((unsigned)shell::FAIL);
        else if(eq(*loglevel, "err", 3))
            verbose.set((unsigned)shell::ERR);
        else if(eq(*loglevel, "warn", 4))
            verbose.set((unsigned)shell::WARN);
        else if(eq(*loglevel, "noti", 4))
            verbose.set((unsigned)shell::NOTIFY);
        else if(eq(*loglevel, "info"))
            verbose.set((unsigned)shell::INFO);
        else if(eq(*loglevel, "debug", 5))
            verbose.set((unsigned)shell::DEBUG0 + atoi(*loglevel + 5));
    }

    if(is(debuglevel))
        verbose.set((unsigned)shell::DEBUG0 + *debuglevel);

#ifdef  HAVE_PWD_H
    pwd = NULL;
    struct group *grp = NULL;

    // if root user, then see if we change permissions...

    if(!getuid()) {
        if(*user) {
            if(atoi(*user))
                pwd = getpwuid(atoi(*user));
            else
                pwd = getpwnam(*user);
            if(!pwd)
                shell::errexit(2, "*** apennine: %s: %s\n", *user,
                    _TEXT("unknown or invalid user id"));
        }
    }

    if(*group) {
        if(atoi(*group))
            grp = getgrgid(atoi(*group));
        else
            grp = getgrnam(*group);
        if(!grp)
            shell::errexit(2, "*** apennine: %s: %s\n", *group,
                _TEXT("unknown or invalid group id"));
    }

    if(grp) {
        umask(007);
        setgid(grp->gr_gid);
    }

    int uid = 0;

    if(pwd) {
        umask(007);
        if(!grp)
            setgid(pwd->pw_gid);
        if(!is(prefixpath) && pwd->pw_dir && *pwd->pw_dir == '/')
            prefixpath.set(pwd->pw_dir);
        uid = pwd->pw_uid;
    }

    endgrent();
    endpwent();

#endif

    // create directories, set prefix, etc...

    dir::create(controlpath, fsys::GROUP_PRIVATE);
    dir::create(prefixpath, fsys::GROUP_PRIVATE);
    if(fsys::prefix(prefixpath))
        shell::errexit(3, "*** apennine: %s: %s\n",
            *prefixpath, _TEXT("data directory unavailable"));

    shell::loglevel_t level = (shell::loglevel_t)*verbose;

    // daemonify process....
    if(daemon) {
        if(!detached)
            args.detach(svc);
        shell::log("apennine", level, shell::CONSOLE_LOG);
    }
    else
        shell::log("apennine", level, shell::SYSTEM_LOG);

    psignals::setup();

    // drop root privilege
#ifdef  HAVE_PWD_H
    if(uid)
        setuid(uid);
#endif

    // load plugins after daemon to avoid file closure, and after log started...

    // process startup...

    if(is(restart))
        args.restart();

    if(pidfile) {
        ::remove(pidfile);
        FILE *fp = fopen(pidfile, "w");
        if(fp) {
            fprintf(fp, "    %ld\n", (long)getpid());
            fclose(fp);
        }
    }
}

} // end namespace

// stub code for windows service daemon...

using namespace apennine;

static SERVICE_MAIN(main, argc, argv)
{
    psignals::service("apennine");
    init(argc, argv, true);
}

PROGRAM_MAIN(argc, argv)
{
    init(argc, argv, false, &service_main);
    if(pidfile)
        ::remove(pidfile);
    PROGRAM_EXIT(0);
}

